/**
* Meta API: WP_Meta_Query class
*
* @package WordPress
* @subpackage Meta
* @since 4.4.0
*/
/**
* Core class used to implement meta queries for the Meta API.
*
* Used for generating SQL clauses that filter a primary query according to metadata keys and values.
*
* WP_Meta_Query is a helper that allows primary query classes, such as WP_Query and WP_User_Query,
*
* to filter their results by object metadata, by generating `JOIN` and `WHERE` subclauses to be attached
* to the primary SQL query string.
*
* @since 3.2.0
*/
#[AllowDynamicProperties]
class WP_Meta_Query {
/**
* Array of metadata queries.
*
* See WP_Meta_Query::__construct() for information on meta query arguments.
*
* @since 3.2.0
* @var array
*/
public $queries = array();
/**
* The relation between the queries. Can be one of 'AND' or 'OR'.
*
* @since 3.2.0
* @var string
*/
public $relation;
/**
* Database table to query for the metadata.
*
* @since 4.1.0
* @var string
*/
public $meta_table;
/**
* Column in meta_table that represents the ID of the object the metadata belongs to.
*
* @since 4.1.0
* @var string
*/
public $meta_id_column;
/**
* Database table that where the metadata's objects are stored (eg $wpdb->users).
*
* @since 4.1.0
* @var string
*/
public $primary_table;
/**
* Column in primary_table that represents the ID of the object.
*
* @since 4.1.0
* @var string
*/
public $primary_id_column;
/**
* A flat list of table aliases used in JOIN clauses.
*
* @since 4.1.0
* @var array
*/
protected $table_aliases = array();
/**
* A flat list of clauses, keyed by clause 'name'.
*
* @since 4.2.0
* @var array
*/
protected $clauses = array();
/**
* Whether the query contains any OR relations.
*
* @since 4.3.0
* @var bool
*/
protected $has_or_relation = false;
/**
* Constructor.
*
* @since 3.2.0
* @since 4.2.0 Introduced support for naming query clauses by associative array keys.
* @since 5.1.0 Introduced `$compare_key` clause parameter, which enables LIKE key matches.
* @since 5.3.0 Increased the number of operators available to `$compare_key`. Introduced `$type_key`,
* which enables the `$key` to be cast to a new data type for comparisons.
*
* @param array $meta_query {
* Array of meta query clauses. When first-order clauses or sub-clauses use strings as
* their array keys, they may be referenced in the 'orderby' parameter of the parent query.
*
* @type string $relation Optional. The MySQL keyword used to join the clauses of the query.
* Accepts 'AND' or 'OR'. Default 'AND'.
* @type array ...$0 {
* Optional. An array of first-order clause parameters, or another fully-formed meta query.
*
* @type string|string[] $key Meta key or keys to filter by.
* @type string $compare_key MySQL operator used for comparing the $key. Accepts:
* - '='
* - '!='
* - 'LIKE'
* - 'NOT LIKE'
* - 'IN'
* - 'NOT IN'
* - 'REGEXP'
* - 'NOT REGEXP'
* - 'RLIKE'
* - 'EXISTS' (alias of '=')
* - 'NOT EXISTS' (alias of '!=')
* Default is 'IN' when `$key` is an array, '=' otherwise.
* @type string $type_key MySQL data type that the meta_key column will be CAST to for
* comparisons. Accepts 'BINARY' for case-sensitive regular expression
* comparisons. Default is ''.
* @type string|string[] $value Meta value or values to filter by.
* @type string $compare MySQL operator used for comparing the $value. Accepts:
* - '='
* - '!='
* - '>'
* - '>='
* - '<'
* - '<='
* - 'LIKE'
* - 'NOT LIKE'
* - 'IN'
* - 'NOT IN'
* - 'BETWEEN'
* - 'NOT BETWEEN'
* - 'REGEXP'
* - 'NOT REGEXP'
* - 'RLIKE'
* - 'EXISTS'
* - 'NOT EXISTS'
* Default is 'IN' when `$value` is an array, '=' otherwise.
* @type string $type MySQL data type that the meta_value column will be CAST to for
* comparisons. Accepts:
* - 'NUMERIC'
* - 'BINARY'
* - 'CHAR'
* - 'DATE'
* - 'DATETIME'
* - 'DECIMAL'
* - 'SIGNED'
* - 'TIME'
* - 'UNSIGNED'
* Default is 'CHAR'.
* }
* }
*/
public function __construct( $meta_query = false ) {
if ( ! $meta_query ) {
return;
}
if ( isset( $meta_query['relation'] ) && 'OR' === strtoupper( $meta_query['relation'] ) ) {
$this->relation = 'OR';
} else {
$this->relation = 'AND';
}
$this->queries = $this->sanitize_query( $meta_query );
}
/**
* Ensures the 'meta_query' argument passed to the class constructor is well-formed.
*
* Eliminates empty items and ensures that a 'relation' is set.
*
* @since 4.1.0
*
* @param array $queries Array of query clauses.
* @return array Sanitized array of query clauses.
*/
public function sanitize_query( $queries ) {
$clean_queries = array();
if ( ! is_array( $queries ) ) {
return $clean_queries;
}
foreach ( $queries as $key => $query ) {
if ( 'relation' === $key ) {
$relation = $query;
} elseif ( ! is_array( $query ) ) {
continue;
// First-order clause.
} elseif ( $this->is_first_order_clause( $query ) ) {
if ( isset( $query['value'] ) && array() === $query['value'] ) {
unset( $query['value'] );
}
$clean_queries[ $key ] = $query;
// Otherwise, it's a nested query, so we recurse.
} else {
$cleaned_query = $this->sanitize_query( $query );
if ( ! empty( $cleaned_query ) ) {
$clean_queries[ $key ] = $cleaned_query;
}
}
}
if ( empty( $clean_queries ) ) {
return $clean_queries;
}
// Sanitize the 'relation' key provided in the query.
if ( isset( $relation ) && 'OR' === strtoupper( $relation ) ) {
$clean_queries['relation'] = 'OR';
$this->has_or_relation = true;
/*
* If there is only a single clause, call the relation 'OR'.
* This value will not actually be used to join clauses, but it
* simplifies the logic around combining key-only queries.
*/
} elseif ( 1 === count( $clean_queries ) ) {
$clean_queries['relation'] = 'OR';
// Default to AND.
} else {
$clean_queries['relation'] = 'AND';
}
return $clean_queries;
}
/**
* Determines whether a query clause is first-order.
*
* A first-order meta query clause is one that has either a 'key' or
* a 'value' array key.
*
* @since 4.1.0
*
* @param array $query Meta query arguments.
* @return bool Whether the query clause is a first-order clause.
*/
protected function is_first_order_clause( $query ) {
return isset( $query['key'] ) || isset( $query['value'] );
}
/**
* Constructs a meta query based on 'meta_*' query vars
*
* @since 3.2.0
*
* @param array $qv The query variables.
*/
public function parse_query_vars( $qv ) {
$meta_query = array();
/*
* For orderby=meta_value to work correctly, simple query needs to be
* first (so that its table join is against an unaliased meta table) and
* needs to be its own clause (so it doesn't interfere with the logic of
* the rest of the meta_query).
*/
$primary_meta_query = array();
foreach ( array( 'key', 'compare', 'type', 'compare_key', 'type_key' ) as $key ) {
if ( ! empty( $qv[ "meta_$key" ] ) ) {
$primary_meta_query[ $key ] = $qv[ "meta_$key" ];
}
}
// WP_Query sets 'meta_value' = '' by default.
if ( isset( $qv['meta_value'] ) && '' !== $qv['meta_value'] && ( ! is_array( $qv['meta_value'] ) || $qv['meta_value'] ) ) {
$primary_meta_query['value'] = $qv['meta_value'];
}
$existing_meta_query = isset( $qv['meta_query'] ) && is_array( $qv['meta_query'] ) ? $qv['meta_query'] : array();
if ( ! empty( $primary_meta_query ) && ! empty( $existing_meta_query ) ) {
$meta_query = array(
'relation' => 'AND',
$primary_meta_query,
$existing_meta_query,
);
} elseif ( ! empty( $primary_meta_query ) ) {
$meta_query = array(
$primary_meta_query,
);
} elseif ( ! empty( $existing_meta_query ) ) {
$meta_query = $existing_meta_query;
}
$this->__construct( $meta_query );
}
/**
* Returns the appropriate alias for the given meta type if applicable.
*
* @since 3.7.0
*
* @param string $type MySQL type to cast meta_value.
* @return string MySQL type.
*/
public function get_cast_for_type( $type = '' ) {
if ( empty( $type ) ) {
return 'CHAR';
}
$meta_type = strtoupper( $type );
if ( ! preg_match( '/^(?:BINARY|CHAR|DATE|DATETIME|SIGNED|UNSIGNED|TIME|NUMERIC(?:\(\d+(?:,\s?\d+)?\))?|DECIMAL(?:\(\d+(?:,\s?\d+)?\))?)$/', $meta_type ) ) {
return 'CHAR';
}
if ( 'NUMERIC' === $meta_type ) {
$meta_type = 'SIGNED';
}
return $meta_type;
}
/**
* Generates SQL clauses to be appended to a main query.
*
* @since 3.2.0
*
* @param string $type Type of meta. Possible values include but are not limited
* to 'post', 'comment', 'blog', 'term', and 'user'.
* @param string $primary_table Database table where the object being filtered is stored (eg wp_users).
* @param string $primary_id_column ID column for the filtered object in $primary_table.
* @param object $context Optional. The main query object that corresponds to the type, for
* example a `WP_Query`, `WP_User_Query`, or `WP_Site_Query`.
* Default null.
* @return string[]|false {
* Array containing JOIN and WHERE SQL clauses to append to the main query,
* or false if no table exists for the requested meta type.
*
* @type string $join SQL fragment to append to the main JOIN clause.
* @type string $where SQL fragment to append to the main WHERE clause.
* }
*/
public function get_sql( $type, $primary_table, $primary_id_column, $context = null ) {
$meta_table = _get_meta_table( $type );
if ( ! $meta_table ) {
return false;
}
$this->table_aliases = array();
$this->meta_table = $meta_table;
$this->meta_id_column = sanitize_key( $type . '_id' );
$this->primary_table = $primary_table;
$this->primary_id_column = $primary_id_column;
$sql = $this->get_sql_clauses();
/*
* If any JOINs are LEFT JOINs (as in the case of NOT EXISTS), then all JOINs should
* be LEFT. Otherwise posts with no metadata will be excluded from results.
*/
if ( str_contains( $sql['join'], 'LEFT JOIN' ) ) {
$sql['join'] = str_replace( 'INNER JOIN', 'LEFT JOIN', $sql['join'] );
}
/**
* Filters the meta query's generated SQL.
*
* @since 3.1.0
*
* @param string[] $sql Array containing the query's JOIN and WHERE clauses.
* @param array $queries Array of meta queries.
* @param string $type Type of meta. Possible values include but are not limited
* to 'post', 'comment', 'blog', 'term', and 'user'.
* @param string $primary_table Primary table.
* @param string $primary_id_column Primary column ID.
* @param object $context The main query object that corresponds to the type, for
* example a `WP_Query`, `WP_User_Query`, or `WP_Site_Query`.
*/
return apply_filters_ref_array( 'get_meta_sql', array( $sql, $this->queries, $type, $primary_table, $primary_id_column, $context ) );
}
/**
* Generates SQL clauses to be appended to a main query.
*
* Called by the public WP_Meta_Query::get_sql(), this method is abstracted
* out to maintain parity with the other Query classes.
*
* @since 4.1.0
*
* @return string[] {
* Array containing JOIN and WHERE SQL clauses to append to the main query.
*
* @type string $join SQL fragment to append to the main JOIN clause.
* @type string $where SQL fragment to append to the main WHERE clause.
* }
*/
protected function get_sql_clauses() {
/*
* $queries are passed by reference to get_sql_for_query() for recursion.
* To keep $this->queries unaltered, pass a copy.
*/
$queries = $this->queries;
$sql = $this->get_sql_for_query( $queries );
if ( ! empty( $sql['where'] ) ) {
$sql['where'] = ' AND ' . $sql['where'];
}
return $sql;
}
/**
* Generates SQL clauses for a single query array.
*
* If nested subqueries are found, this method recurses the tree to
* produce the properly nested SQL.
*
* @since 4.1.0
*
* @param array $query Query to parse (passed by reference).
* @param int $depth Optional. Number of tree levels deep we currently are.
* Used to calculate indentation. Default 0.
* @return string[] {
* Array containing JOIN and WHERE SQL clauses to append to a single query array.
*
* @type string $join SQL fragment to append to the main JOIN clause.
* @type string $where SQL fragment to append to the main WHERE clause.
* }
*/
protected function get_sql_for_query( &$query, $depth = 0 ) {
$sql_chunks = array(
'join' => array(),
'where' => array(),
);
$sql = array(
'join' => '',
'where' => '',
);
$indent = '';
for ( $i = 0; $i < $depth; $i++ ) {
$indent .= ' ';
}
foreach ( $query as $key => &$clause ) {
if ( 'relation' === $key ) {
$relation = $query['relation'];
} elseif ( is_array( $clause ) ) {
// This is a first-order clause.
if ( $this->is_first_order_clause( $clause ) ) {
$clause_sql = $this->get_sql_for_clause( $clause, $query, $key );
$where_count = count( $clause_sql['where'] );
if ( ! $where_count ) {
$sql_chunks['where'][] = '';
} elseif ( 1 === $where_count ) {
$sql_chunks['where'][] = $clause_sql['where'][0];
} else {
$sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )';
}
$sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] );
// This is a subquery, so we recurse.
} else {
$clause_sql = $this->get_sql_for_query( $clause, $depth + 1 );
$sql_chunks['where'][] = $clause_sql['where'];
$sql_chunks['join'][] = $clause_sql['join'];
}
}
}
// Filter to remove empties.
$sql_chunks['join'] = array_filter( $sql_chunks['join'] );
$sql_chunks['where'] = array_filter( $sql_chunks['where'] );
if ( empty( $relation ) ) {
$relation = 'AND';
}
// Filter duplicate JOIN clauses and combine into a single string.
if ( ! empty( $sql_chunks['join'] ) ) {
$sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) );
}
// Generate a single WHERE clause with proper brackets and indentation.
if ( ! empty( $sql_chunks['where'] ) ) {
$sql['where'] = '( ' . "\n " . $indent . implode( ' ' . "\n " . $indent . $relation . ' ' . "\n " . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')';
}
return $sql;
}
/**
* Generates SQL JOIN and WHERE clauses for a first-order query clause.
*
* "First-order" means that it's an array with a 'key' or 'value'.
*
* @since 4.1.0
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param array $clause Query clause (passed by reference).
* @param array $parent_query Parent query array.
* @param string $clause_key Optional. The array key used to name the clause in the original `$meta_query`
* parameters. If not provided, a key will be generated automatically.
* Default empty string.
* @return array {
* Array containing JOIN and WHERE SQL clauses to append to a first-order query.
*
* @type string[] $join Array of SQL fragments to append to the main JOIN clause.
* @type string[] $where Array of SQL fragments to append to the main WHERE clause.
* }
*/
public function get_sql_for_clause( &$clause, $parent_query, $clause_key = '' ) {
global $wpdb;
$sql_chunks = array(
'where' => array(),
'join' => array(),
);
if ( isset( $clause['compare'] ) ) {
$clause['compare'] = strtoupper( $clause['compare'] );
} else {
$clause['compare'] = isset( $clause['value'] ) && is_array( $clause['value'] ) ? 'IN' : '=';
}
$non_numeric_operators = array(
'=',
'!=',
'LIKE',
'NOT LIKE',
'IN',
'NOT IN',
'EXISTS',
'NOT EXISTS',
'RLIKE',
'REGEXP',
'NOT REGEXP',
);
$numeric_operators = array(
'>',
'>=',
'<',
'<=',
'BETWEEN',
'NOT BETWEEN',
);
if ( ! in_array( $clause['compare'], $non_numeric_operators, true ) && ! in_array( $clause['compare'], $numeric_operators, true ) ) {
$clause['compare'] = '=';
}
if ( isset( $clause['compare_key'] ) ) {
$clause['compare_key'] = strtoupper( $clause['compare_key'] );
} else {
$clause['compare_key'] = isset( $clause['key'] ) && is_array( $clause['key'] ) ? 'IN' : '=';
}
if ( ! in_array( $clause['compare_key'], $non_numeric_operators, true ) ) {
$clause['compare_key'] = '=';
}
$meta_compare = $clause['compare'];
$meta_compare_key = $clause['compare_key'];
// First build the JOIN clause, if one is required.
$join = '';
// We prefer to avoid joins if possible. Look for an existing join compatible with this clause.
$alias = $this->find_compatible_table_alias( $clause, $parent_query );
if ( false === $alias ) {
$i = count( $this->table_aliases );
$alias = $i ? 'mt' . $i : $this->meta_table;
// JOIN clauses for NOT EXISTS have their own syntax.
if ( 'NOT EXISTS' === $meta_compare ) {
$join .= " LEFT JOIN $this->meta_table";
$join .= $i ? " AS $alias" : '';
if ( 'LIKE' === $meta_compare_key ) {
$join .= $wpdb->prepare( " ON ( $this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column AND $alias.meta_key LIKE %s )", '%' . $wpdb->esc_like( $clause['key'] ) . '%' );
} else {
$join .= $wpdb->prepare( " ON ( $this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column AND $alias.meta_key = %s )", $clause['key'] );
}
// All other JOIN clauses.
} else {
$join .= " INNER JOIN $this->meta_table";
$join .= $i ? " AS $alias" : '';
$join .= " ON ( $this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column )";
}
$this->table_aliases[] = $alias;
$sql_chunks['join'][] = $join;
}
// Save the alias to this clause, for future siblings to find.
$clause['alias'] = $alias;
// Determine the data type.
$_meta_type = isset( $clause['type'] ) ? $clause['type'] : '';
$meta_type = $this->get_cast_for_type( $_meta_type );
$clause['cast'] = $meta_type;
// Fallback for clause keys is the table alias. Key must be a string.
if ( is_int( $clause_key ) || ! $clause_key ) {
$clause_key = $clause['alias'];
}
// Ensure unique clause keys, so none are overwritten.
$iterator = 1;
$clause_key_base = $clause_key;
while ( isset( $this->clauses[ $clause_key ] ) ) {
$clause_key = $clause_key_base . '-' . $iterator;
++$iterator;
}
// Store the clause in our flat array.
$this->clauses[ $clause_key ] =& $clause;
// Next, build the WHERE clause.
// meta_key.
if ( array_key_exists( 'key', $clause ) ) {
if ( 'NOT EXISTS' === $meta_compare ) {
$sql_chunks['where'][] = $alias . '.' . $this->meta_id_column . ' IS NULL';
} else {
/**
* In joined clauses negative operators have to be nested into a
* NOT EXISTS clause and flipped, to avoid returning records with
* matching post IDs but different meta keys. Here we prepare the
* nested clause.
*/
if ( in_array( $meta_compare_key, array( '!=', 'NOT IN', 'NOT LIKE', 'NOT EXISTS', 'NOT REGEXP' ), true ) ) {
// Negative clauses may be reused.
$i = count( $this->table_aliases );
$subquery_alias = $i ? 'mt' . $i : $this->meta_table;
$this->table_aliases[] = $subquery_alias;
$meta_compare_string_start = 'NOT EXISTS (';
$meta_compare_string_start .= "SELECT 1 FROM $wpdb->postmeta $subquery_alias ";
$meta_compare_string_start .= "WHERE $subquery_alias.post_ID = $alias.post_ID ";
$meta_compare_string_end = 'LIMIT 1';
$meta_compare_string_end .= ')';
}
switch ( $meta_compare_key ) {
case '=':
case 'EXISTS':
$where = $wpdb->prepare( "$alias.meta_key = %s", trim( $clause['key'] ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
break;
case 'LIKE':
$meta_compare_value = '%' . $wpdb->esc_like( trim( $clause['key'] ) ) . '%';
$where = $wpdb->prepare( "$alias.meta_key LIKE %s", $meta_compare_value ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
break;
case 'IN':
$meta_compare_string = "$alias.meta_key IN (" . substr( str_repeat( ',%s', count( $clause['key'] ) ), 1 ) . ')';
$where = $wpdb->prepare( $meta_compare_string, $clause['key'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
break;
case 'RLIKE':
case 'REGEXP':
$operator = $meta_compare_key;
if ( isset( $clause['type_key'] ) && 'BINARY' === strtoupper( $clause['type_key'] ) ) {
$cast = 'BINARY';
$meta_key = "CAST($alias.meta_key AS BINARY)";
} else {
$cast = '';
$meta_key = "$alias.meta_key";
}
$where = $wpdb->prepare( "$meta_key $operator $cast %s", trim( $clause['key'] ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
break;
case '!=':
case 'NOT EXISTS':
$meta_compare_string = $meta_compare_string_start . "AND $subquery_alias.meta_key = %s " . $meta_compare_string_end;
$where = $wpdb->prepare( $meta_compare_string, $clause['key'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
break;
case 'NOT LIKE':
$meta_compare_string = $meta_compare_string_start . "AND $subquery_alias.meta_key LIKE %s " . $meta_compare_string_end;
$meta_compare_value = '%' . $wpdb->esc_like( trim( $clause['key'] ) ) . '%';
$where = $wpdb->prepare( $meta_compare_string, $meta_compare_value ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
break;
case 'NOT IN':
$array_subclause = '(' . substr( str_repeat( ',%s', count( $clause['key'] ) ), 1 ) . ') ';
$meta_compare_string = $meta_compare_string_start . "AND $subquery_alias.meta_key IN " . $array_subclause . $meta_compare_string_end;
$where = $wpdb->prepare( $meta_compare_string, $clause['key'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
break;
case 'NOT REGEXP':
$operator = $meta_compare_key;
if ( isset( $clause['type_key'] ) && 'BINARY' === strtoupper( $clause['type_key'] ) ) {
$cast = 'BINARY';
$meta_key = "CAST($subquery_alias.meta_key AS BINARY)";
} else {
$cast = '';
$meta_key = "$subquery_alias.meta_key";
}
$meta_compare_string = $meta_compare_string_start . "AND $meta_key REGEXP $cast %s " . $meta_compare_string_end;
$where = $wpdb->prepare( $meta_compare_string, $clause['key'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
break;
}
$sql_chunks['where'][] = $where;
}
}
// meta_value.
if ( array_key_exists( 'value', $clause ) ) {
$meta_value = $clause['value'];
if ( in_array( $meta_compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ), true ) ) {
if ( ! is_array( $meta_value ) ) {
$meta_value = preg_split( '/[,\s]+/', $meta_value );
}
} elseif ( is_string( $meta_value ) ) {
$meta_value = trim( $meta_value );
}
switch ( $meta_compare ) {
case 'IN':
case 'NOT IN':
$meta_compare_string = '(' . substr( str_repeat( ',%s', count( $meta_value ) ), 1 ) . ')';
$where = $wpdb->prepare( $meta_compare_string, $meta_value );
break;
case 'BETWEEN':
case 'NOT BETWEEN':
$where = $wpdb->prepare( '%s AND %s', $meta_value[0], $meta_value[1] );
break;
case 'LIKE':
case 'NOT LIKE':
$meta_value = '%' . $wpdb->esc_like( $meta_value ) . '%';
$where = $wpdb->prepare( '%s', $meta_value );
break;
// EXISTS with a value is interpreted as '='.
case 'EXISTS':
$meta_compare = '=';
$where = $wpdb->prepare( '%s', $meta_value );
break;
// 'value' is ignored for NOT EXISTS.
case 'NOT EXISTS':
$where = '';
break;
default:
$where = $wpdb->prepare( '%s', $meta_value );
break;
}
if ( $where ) {
if ( 'CHAR' === $meta_type ) {
$sql_chunks['where'][] = "$alias.meta_value {$meta_compare} {$where}";
} else {
$sql_chunks['where'][] = "CAST($alias.meta_value AS {$meta_type}) {$meta_compare} {$where}";
}
}
}
/*
* Multiple WHERE clauses (for meta_key and meta_value) should
* be joined in parentheses.
*/
if ( 1 < count( $sql_chunks['where'] ) ) {
$sql_chunks['where'] = array( '( ' . implode( ' AND ', $sql_chunks['where'] ) . ' )' );
}
return $sql_chunks;
}
/**
* Gets a flattened list of sanitized meta clauses.
*
* This array should be used for clause lookup, as when the table alias and CAST type must be determined for
* a value of 'orderby' corresponding to a meta clause.
*
* @since 4.2.0
*
* @return array Meta clauses.
*/
public function get_clauses() {
return $this->clauses;
}
/**
* Identifies an existing table alias that is compatible with the current
* query clause.
*
* We avoid unnecessary table joins by allowing each clause to look for
* an existing table alias that is compatible with the query that it
* needs to perform.
*
* An existing alias is compatible if (a) it is a sibling of `$clause`
* (ie, it's under the scope of the same relation), and (b) the combination
* of operator and relation between the clauses allows for a shared table join.
* In the case of WP_Meta_Query, this only applies to 'IN' clauses that are
* connected by the relation 'OR'.
*
* @since 4.1.0
*
* @param array $clause Query clause.
* @param array $parent_query Parent query of $clause.
* @return string|false Table alias if found, otherwise false.
*/
protected function find_compatible_table_alias( $clause, $parent_query ) {
$alias = false;
foreach ( $parent_query as $sibling ) {
// If the sibling has no alias yet, there's nothing to check.
if ( empty( $sibling['alias'] ) ) {
continue;
}
// We're only interested in siblings that are first-order clauses.
if ( ! is_array( $sibling ) || ! $this->is_first_order_clause( $sibling ) ) {
continue;
}
$compatible_compares = array();
// Clauses connected by OR can share joins as long as they have "positive" operators.
if ( 'OR' === $parent_query['relation'] ) {
$compatible_compares = array( '=', 'IN', 'BETWEEN', 'LIKE', 'REGEXP', 'RLIKE', '>', '>=', '<', '<=' );
// Clauses joined by AND with "negative" operators share a join only if they also share a key.
} elseif ( isset( $sibling['key'] ) && isset( $clause['key'] ) && $sibling['key'] === $clause['key'] ) {
$compatible_compares = array( '!=', 'NOT IN', 'NOT LIKE' );
}
$clause_compare = strtoupper( $clause['compare'] );
$sibling_compare = strtoupper( $sibling['compare'] );
if ( in_array( $clause_compare, $compatible_compares, true ) && in_array( $sibling_compare, $compatible_compares, true ) ) {
$alias = preg_replace( '/\W/', '_', $sibling['alias'] );
break;
}
}
/**
* Filters the table alias identified as compatible with the current clause.
*
* @since 4.1.0
*
* @param string|false $alias Table alias, or false if none was found.
* @param array $clause First-order query clause.
* @param array $parent_query Parent of $clause.
* @param WP_Meta_Query $query WP_Meta_Query object.
*/
return apply_filters( 'meta_query_find_compatible_table_alias', $alias, $clause, $parent_query, $this );
}
/**
* Checks whether the current query has any OR relations.
*
* In some cases, the presence of an OR relation somewhere in the query will require
* the use of a `DISTINCT` or `GROUP BY` keyword in the `SELECT` clause. The current
* method can be used in these cases to determine whether such a clause is necessary.
*
* @since 4.3.0
*
* @return bool True if the query contains any `OR` relations, otherwise false.
*/
public function has_or_relation() {
return $this->has_or_relation;
}
}
/**
* WordPress Post Template Functions.
*
* Gets content for the current post in the loop.
*
* @package WordPress
* @subpackage Template
*/
/**
* Displays the ID of the current item in the WordPress Loop.
*
* @since 0.71
*/
function the_ID() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
echo get_the_ID();
}
/**
* Retrieves the ID of the current item in the WordPress Loop.
*
* @since 2.1.0
*
* @return int|false The ID of the current item in the WordPress Loop. False if $post is not set.
*/
function get_the_ID() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
$post = get_post();
return ! empty( $post ) ? $post->ID : false;
}
/**
* Displays or retrieves the current post title with optional markup.
*
* @since 0.71
*
* @param string $before Optional. Markup to prepend to the title. Default empty.
* @param string $after Optional. Markup to append to the title. Default empty.
* @param bool $display Optional. Whether to echo or return the title. Default true for echo.
* @return void|string Void if `$display` argument is true or the title is empty,
* current post title if `$display` is false.
*/
function the_title( $before = '', $after = '', $display = true ) {
$title = get_the_title();
if ( strlen( $title ) === 0 ) {
return;
}
$title = $before . $title . $after;
if ( $display ) {
echo $title;
} else {
return $title;
}
}
/**
* Sanitizes the current title when retrieving or displaying.
*
* Works like the_title(), except the parameters can be in a string or
* an array. See the function for what can be override in the $args parameter.
*
* The title before it is displayed will have the tags stripped and esc_attr()
* before it is passed to the user or displayed. The default as with the_title(),
* is to display the title.
*
* @since 2.3.0
*
* @param string|array $args {
* Title attribute arguments. Optional.
*
* @type string $before Markup to prepend to the title. Default empty.
* @type string $after Markup to append to the title. Default empty.
* @type bool $echo Whether to echo or return the title. Default true for echo.
* @type WP_Post $post Current post object to retrieve the title for.
* }
* @return void|string Void if 'echo' argument is true, the title attribute if 'echo' is false.
*/
function the_title_attribute( $args = '' ) {
$defaults = array(
'before' => '',
'after' => '',
'echo' => true,
'post' => get_post(),
);
$parsed_args = wp_parse_args( $args, $defaults );
$title = get_the_title( $parsed_args['post'] );
if ( strlen( $title ) === 0 ) {
return;
}
$title = $parsed_args['before'] . $title . $parsed_args['after'];
$title = esc_attr( strip_tags( $title ) );
if ( $parsed_args['echo'] ) {
echo $title;
} else {
return $title;
}
}
/**
* Retrieves the post title.
*
* If the post is protected and the visitor is not an admin, then "Protected"
* will be inserted before the post title. If the post is private, then
* "Private" will be inserted before the post title.
*
* @since 0.71
*
* @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post.
* @return string
*/
function get_the_title( $post = 0 ) {
$post = get_post( $post );
$post_title = isset( $post->post_title ) ? $post->post_title : '';
$post_id = isset( $post->ID ) ? $post->ID : 0;
if ( ! is_admin() ) {
if ( ! empty( $post->post_password ) ) {
/* translators: %s: Protected post title. */
$prepend = __( 'Protected: %s' );
/**
* Filters the text prepended to the post title for protected posts.
*
* The filter is only applied on the front end.
*
* @since 2.8.0
*
* @param string $prepend Text displayed before the post title.
* Default 'Protected: %s'.
* @param WP_Post $post Current post object.
*/
$protected_title_format = apply_filters( 'protected_title_format', $prepend, $post );
$post_title = sprintf( $protected_title_format, $post_title );
} elseif ( isset( $post->post_status ) && 'private' === $post->post_status ) {
/* translators: %s: Private post title. */
$prepend = __( 'Private: %s' );
/**
* Filters the text prepended to the post title of private posts.
*
* The filter is only applied on the front end.
*
* @since 2.8.0
*
* @param string $prepend Text displayed before the post title.
* Default 'Private: %s'.
* @param WP_Post $post Current post object.
*/
$private_title_format = apply_filters( 'private_title_format', $prepend, $post );
$post_title = sprintf( $private_title_format, $post_title );
}
}
/**
* Filters the post title.
*
* @since 0.71
*
* @param string $post_title The post title.
* @param int $post_id The post ID.
*/
return apply_filters( 'the_title', $post_title, $post_id );
}
/**
* Displays the Post Global Unique Identifier (guid).
*
* The guid will appear to be a link, but should not be used as a link to the
* post. The reason you should not use it as a link, is because of moving the
* blog across domains.
*
* URL is escaped to make it XML-safe.
*
* @since 1.5.0
*
* @param int|WP_Post $post Optional. Post ID or post object. Default is global $post.
*/
function the_guid( $post = 0 ) {
$post = get_post( $post );
$post_guid = isset( $post->guid ) ? get_the_guid( $post ) : '';
$post_id = isset( $post->ID ) ? $post->ID : 0;
/**
* Filters the escaped Global Unique Identifier (guid) of the post.
*
* @since 4.2.0
*
* @see get_the_guid()
*
* @param string $post_guid Escaped Global Unique Identifier (guid) of the post.
* @param int $post_id The post ID.
*/
echo apply_filters( 'the_guid', $post_guid, $post_id );
}
/**
* Retrieves the Post Global Unique Identifier (guid).
*
* The guid will appear to be a link, but should not be used as an link to the
* post. The reason you should not use it as a link, is because of moving the
* blog across domains.
*
* @since 1.5.0
*
* @param int|WP_Post $post Optional. Post ID or post object. Default is global $post.
* @return string
*/
function get_the_guid( $post = 0 ) {
$post = get_post( $post );
$post_guid = isset( $post->guid ) ? $post->guid : '';
$post_id = isset( $post->ID ) ? $post->ID : 0;
/**
* Filters the Global Unique Identifier (guid) of the post.
*
* @since 1.5.0
*
* @param string $post_guid Global Unique Identifier (guid) of the post.
* @param int $post_id The post ID.
*/
return apply_filters( 'get_the_guid', $post_guid, $post_id );
}
/**
* Displays the post content.
*
* @since 0.71
*
* @param string $more_link_text Optional. Content for when there is more text.
* @param bool $strip_teaser Optional. Strip teaser content before the more text. Default false.
*/
function the_content( $more_link_text = null, $strip_teaser = false ) {
$content = get_the_content( $more_link_text, $strip_teaser );
/**
* Filters the post content.
*
* @since 0.71
*
* @param string $content Content of the current post.
*/
$content = apply_filters( 'the_content', $content );
$content = str_replace( ']]>', ']]>', $content );
echo $content;
}
/**
* Retrieves the post content.
*
* @since 0.71
* @since 5.2.0 Added the `$post` parameter.
*
* @global int $page Page number of a single post/page.
* @global int $more Boolean indicator for whether single post/page is being viewed.
* @global bool $preview Whether post/page is in preview mode.
* @global array $pages Array of all pages in post/page. Each array element contains
* part of the content separated by the `` tag.
* @global int $multipage Boolean indicator for whether multiple pages are in play.
*
* @param string $more_link_text Optional. Content for when there is more text.
* @param bool $strip_teaser Optional. Strip teaser content before the more text. Default false.
* @param WP_Post|object|int $post Optional. WP_Post instance or Post ID/object. Default null.
* @return string
*/
function get_the_content( $more_link_text = null, $strip_teaser = false, $post = null ) {
global $page, $more, $preview, $pages, $multipage;
$_post = get_post( $post );
if ( ! ( $_post instanceof WP_Post ) ) {
return '';
}
/*
* Use the globals if the $post parameter was not specified,
* but only after they have been set up in setup_postdata().
*/
if ( null === $post && did_action( 'the_post' ) ) {
$elements = compact( 'page', 'more', 'preview', 'pages', 'multipage' );
} else {
$elements = generate_postdata( $_post );
}
if ( null === $more_link_text ) {
$more_link_text = sprintf(
'%2$s',
sprintf(
/* translators: %s: Post title. */
__( 'Continue reading %s' ),
the_title_attribute(
array(
'echo' => false,
'post' => $_post,
)
)
),
__( '(more…)' )
);
}
$output = '';
$has_teaser = false;
// If post password required and it doesn't match the cookie.
if ( post_password_required( $_post ) ) {
return get_the_password_form( $_post );
}
// If the requested page doesn't exist.
if ( $elements['page'] > count( $elements['pages'] ) ) {
// Give them the highest numbered page that DOES exist.
$elements['page'] = count( $elements['pages'] );
}
$page_no = $elements['page'];
$content = $elements['pages'][ $page_no - 1 ];
if ( preg_match( '//', $content, $matches ) ) {
if ( has_block( 'more', $content ) ) {
// Remove the core/more block delimiters. They will be left over after $content is split up.
$content = preg_replace( '//', '', $content );
}
$content = explode( $matches[0], $content, 2 );
if ( ! empty( $matches[1] ) && ! empty( $more_link_text ) ) {
$more_link_text = strip_tags( wp_kses_no_null( trim( $matches[1] ) ) );
}
$has_teaser = true;
} else {
$content = array( $content );
}
if ( str_contains( $_post->post_content, '' )
&& ( ! $elements['multipage'] || 1 === $elements['page'] )
) {
$strip_teaser = true;
}
$teaser = $content[0];
if ( $elements['more'] && $strip_teaser && $has_teaser ) {
$teaser = '';
}
$output .= $teaser;
if ( count( $content ) > 1 ) {
if ( $elements['more'] ) {
$output .= '' . $content[1];
} else {
if ( ! empty( $more_link_text ) ) {
/**
* Filters the Read More link text.
*
* @since 2.8.0
*
* @param string $more_link_element Read More link element.
* @param string $more_link_text Read More text.
*/
$output .= apply_filters( 'the_content_more_link', ' ID}\" class=\"more-link\">$more_link_text", $more_link_text );
}
$output = force_balance_tags( $output );
}
}
return $output;
}
/**
* Displays the post excerpt.
*
* @since 0.71
*/
function the_excerpt() {
/**
* Filters the displayed post excerpt.
*
* @since 0.71
*
* @see get_the_excerpt()
*
* @param string $post_excerpt The post excerpt.
*/
echo apply_filters( 'the_excerpt', get_the_excerpt() );
}
/**
* Retrieves the post excerpt.
*
* @since 0.71
* @since 4.5.0 Introduced the `$post` parameter.
*
* @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post.
* @return string Post excerpt.
*/
function get_the_excerpt( $post = null ) {
if ( is_bool( $post ) ) {
_deprecated_argument( __FUNCTION__, '2.3.0' );
}
$post = get_post( $post );
if ( empty( $post ) ) {
return '';
}
if ( post_password_required( $post ) ) {
return __( 'There is no excerpt because this is a protected post.' );
}
/**
* Filters the retrieved post excerpt.
*
* @since 1.2.0
* @since 4.5.0 Introduced the `$post` parameter.
*
* @param string $post_excerpt The post excerpt.
* @param WP_Post $post Post object.
*/
return apply_filters( 'get_the_excerpt', $post->post_excerpt, $post );
}
/**
* Determines whether the post has a custom excerpt.
*
* For more information on this and similar theme functions, check out
* the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
* Conditional Tags} article in the Theme Developer Handbook.
*
* @since 2.3.0
*
* @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post.
* @return bool True if the post has a custom excerpt, false otherwise.
*/
function has_excerpt( $post = 0 ) {
$post = get_post( $post );
return ( ! empty( $post->post_excerpt ) );
}
/**
* Displays the classes for the post container element.
*
* @since 2.7.0
*
* @param string|string[] $css_class Optional. One or more classes to add to the class list.
* Default empty.
* @param int|WP_Post $post Optional. Post ID or post object. Defaults to the global `$post`.
*/
function post_class( $css_class = '', $post = null ) {
// Separates classes with a single space, collates classes for post DIV.
echo 'class="' . esc_attr( implode( ' ', get_post_class( $css_class, $post ) ) ) . '"';
}
/**
* Retrieves an array of the class names for the post container element.
*
* The class names are many:
*
* - If the post has a post thumbnail, `has-post-thumbnail` is added as a class.
* - If the post is sticky, then the `sticky` class name is added.
* - The class `hentry` is always added to each post.
* - For each taxonomy that the post belongs to, a class will be added of the format
* `{$taxonomy}-{$slug}`, e.g. `category-foo` or `my_custom_taxonomy-bar`.
* The `post_tag` taxonomy is a special case; the class has the `tag-` prefix
* instead of `post_tag-`.
*
* All class names are passed through the filter, {@see 'post_class'}, followed by
* `$css_class` parameter value, with the post ID as the last parameter.
*
* @since 2.7.0
* @since 4.2.0 Custom taxonomy class names were added.
*
* @param string|string[] $css_class Optional. Space-separated string or array of class names
* to add to the class list. Default empty.
* @param int|WP_Post $post Optional. Post ID or post object.
* @return string[] Array of class names.
*/
function get_post_class( $css_class = '', $post = null ) {
$post = get_post( $post );
$classes = array();
if ( $css_class ) {
if ( ! is_array( $css_class ) ) {
$css_class = preg_split( '#\s+#', $css_class );
}
$classes = array_map( 'esc_attr', $css_class );
} else {
// Ensure that we always coerce class to being an array.
$css_class = array();
}
if ( ! $post ) {
return $classes;
}
$classes[] = 'post-' . $post->ID;
if ( ! is_admin() ) {
$classes[] = $post->post_type;
}
$classes[] = 'type-' . $post->post_type;
$classes[] = 'status-' . $post->post_status;
// Post Format.
if ( post_type_supports( $post->post_type, 'post-formats' ) ) {
$post_format = get_post_format( $post->ID );
if ( $post_format && ! is_wp_error( $post_format ) ) {
$classes[] = 'format-' . sanitize_html_class( $post_format );
} else {
$classes[] = 'format-standard';
}
}
$post_password_required = post_password_required( $post->ID );
// Post requires password.
if ( $post_password_required ) {
$classes[] = 'post-password-required';
} elseif ( ! empty( $post->post_password ) ) {
$classes[] = 'post-password-protected';
}
// Post thumbnails.
if ( current_theme_supports( 'post-thumbnails' ) && has_post_thumbnail( $post->ID ) && ! is_attachment( $post ) && ! $post_password_required ) {
$classes[] = 'has-post-thumbnail';
}
// Sticky for Sticky Posts.
if ( is_sticky( $post->ID ) ) {
if ( is_home() && ! is_paged() ) {
$classes[] = 'sticky';
} elseif ( is_admin() ) {
$classes[] = 'status-sticky';
}
}
// hentry for hAtom compliance.
$classes[] = 'hentry';
// All public taxonomies.
$taxonomies = get_taxonomies( array( 'public' => true ) );
/**
* Filters the taxonomies to generate classes for each individual term.
*
* Default is all public taxonomies registered to the post type.
*
* @since 6.1.0
*
* @param string[] $taxonomies List of all taxonomy names to generate classes for.
* @param int $post_id The post ID.
* @param string[] $classes An array of post class names.
* @param string[] $css_class An array of additional class names added to the post.
*/
$taxonomies = apply_filters( 'post_class_taxonomies', $taxonomies, $post->ID, $classes, $css_class );
foreach ( (array) $taxonomies as $taxonomy ) {
if ( is_object_in_taxonomy( $post->post_type, $taxonomy ) ) {
foreach ( (array) get_the_terms( $post->ID, $taxonomy ) as $term ) {
if ( empty( $term->slug ) ) {
continue;
}
$term_class = sanitize_html_class( $term->slug, $term->term_id );
if ( is_numeric( $term_class ) || ! trim( $term_class, '-' ) ) {
$term_class = $term->term_id;
}
// 'post_tag' uses the 'tag' prefix for backward compatibility.
if ( 'post_tag' === $taxonomy ) {
$classes[] = 'tag-' . $term_class;
} else {
$classes[] = sanitize_html_class( $taxonomy . '-' . $term_class, $taxonomy . '-' . $term->term_id );
}
}
}
}
$classes = array_map( 'esc_attr', $classes );
/**
* Filters the list of CSS class names for the current post.
*
* @since 2.7.0
*
* @param string[] $classes An array of post class names.
* @param string[] $css_class An array of additional class names added to the post.
* @param int $post_id The post ID.
*/
$classes = apply_filters( 'post_class', $classes, $css_class, $post->ID );
return array_unique( $classes );
}
/**
* Displays the class names for the body element.
*
* @since 2.8.0
*
* @param string|string[] $css_class Optional. Space-separated string or array of class names
* to add to the class list. Default empty.
*/
function body_class( $css_class = '' ) {
// Separates class names with a single space, collates class names for body element.
echo 'class="' . esc_attr( implode( ' ', get_body_class( $css_class ) ) ) . '"';
}
/**
* Retrieves an array of the class names for the body element.
*
* @since 2.8.0
*
* @global WP_Query $wp_query WordPress Query object.
*
* @param string|string[] $css_class Optional. Space-separated string or array of class names
* to add to the class list. Default empty.
* @return string[] Array of class names.
*/
function get_body_class( $css_class = '' ) {
global $wp_query;
$classes = array();
if ( is_rtl() ) {
$classes[] = 'rtl';
}
if ( is_front_page() ) {
$classes[] = 'home';
}
if ( is_home() ) {
$classes[] = 'blog';
}
if ( is_privacy_policy() ) {
$classes[] = 'privacy-policy';
}
if ( is_archive() ) {
$classes[] = 'archive';
}
if ( is_date() ) {
$classes[] = 'date';
}
if ( is_search() ) {
$classes[] = 'search';
$classes[] = $wp_query->posts ? 'search-results' : 'search-no-results';
}
if ( is_paged() ) {
$classes[] = 'paged';
}
if ( is_attachment() ) {
$classes[] = 'attachment';
}
if ( is_404() ) {
$classes[] = 'error404';
}
if ( is_singular() ) {
$post = $wp_query->get_queried_object();
$post_id = $post->ID;
$post_type = $post->post_type;
if ( is_page_template() ) {
$classes[] = "{$post_type}-template";
$template_slug = get_page_template_slug( $post_id );
$template_parts = explode( '/', $template_slug );
foreach ( $template_parts as $part ) {
$classes[] = "{$post_type}-template-" . sanitize_html_class( str_replace( array( '.', '/' ), '-', basename( $part, '.php' ) ) );
}
$classes[] = "{$post_type}-template-" . sanitize_html_class( str_replace( '.', '-', $template_slug ) );
} else {
$classes[] = "{$post_type}-template-default";
}
if ( is_single() ) {
$classes[] = 'single';
if ( isset( $post->post_type ) ) {
$classes[] = 'single-' . sanitize_html_class( $post->post_type, $post_id );
$classes[] = 'postid-' . $post_id;
// Post Format.
if ( post_type_supports( $post->post_type, 'post-formats' ) ) {
$post_format = get_post_format( $post->ID );
if ( $post_format && ! is_wp_error( $post_format ) ) {
$classes[] = 'single-format-' . sanitize_html_class( $post_format );
} else {
$classes[] = 'single-format-standard';
}
}
}
}
if ( is_attachment() ) {
$mime_type = get_post_mime_type( $post_id );
$mime_prefix = array( 'application/', 'image/', 'text/', 'audio/', 'video/', 'music/' );
$classes[] = 'attachmentid-' . $post_id;
$classes[] = 'attachment-' . str_replace( $mime_prefix, '', $mime_type );
} elseif ( is_page() ) {
$classes[] = 'page';
$classes[] = 'page-id-' . $post_id;
if ( get_pages(
array(
'parent' => $post_id,
'number' => 1,
)
) ) {
$classes[] = 'page-parent';
}
if ( $post->post_parent ) {
$classes[] = 'page-child';
$classes[] = 'parent-pageid-' . $post->post_parent;
}
}
} elseif ( is_archive() ) {
if ( is_post_type_archive() ) {
$classes[] = 'post-type-archive';
$post_type = get_query_var( 'post_type' );
if ( is_array( $post_type ) ) {
$post_type = reset( $post_type );
}
$classes[] = 'post-type-archive-' . sanitize_html_class( $post_type );
} elseif ( is_author() ) {
$author = $wp_query->get_queried_object();
$classes[] = 'author';
if ( isset( $author->user_nicename ) ) {
$classes[] = 'author-' . sanitize_html_class( $author->user_nicename, $author->ID );
$classes[] = 'author-' . $author->ID;
}
} elseif ( is_category() ) {
$cat = $wp_query->get_queried_object();
$classes[] = 'category';
if ( isset( $cat->term_id ) ) {
$cat_class = sanitize_html_class( $cat->slug, $cat->term_id );
if ( is_numeric( $cat_class ) || ! trim( $cat_class, '-' ) ) {
$cat_class = $cat->term_id;
}
$classes[] = 'category-' . $cat_class;
$classes[] = 'category-' . $cat->term_id;
}
} elseif ( is_tag() ) {
$tag = $wp_query->get_queried_object();
$classes[] = 'tag';
if ( isset( $tag->term_id ) ) {
$tag_class = sanitize_html_class( $tag->slug, $tag->term_id );
if ( is_numeric( $tag_class ) || ! trim( $tag_class, '-' ) ) {
$tag_class = $tag->term_id;
}
$classes[] = 'tag-' . $tag_class;
$classes[] = 'tag-' . $tag->term_id;
}
} elseif ( is_tax() ) {
$term = $wp_query->get_queried_object();
if ( isset( $term->term_id ) ) {
$term_class = sanitize_html_class( $term->slug, $term->term_id );
if ( is_numeric( $term_class ) || ! trim( $term_class, '-' ) ) {
$term_class = $term->term_id;
}
$classes[] = 'tax-' . sanitize_html_class( $term->taxonomy );
$classes[] = 'term-' . $term_class;
$classes[] = 'term-' . $term->term_id;
}
}
}
if ( is_user_logged_in() ) {
$classes[] = 'logged-in';
}
if ( is_admin_bar_showing() ) {
$classes[] = 'admin-bar';
$classes[] = 'no-customize-support';
}
if ( current_theme_supports( 'custom-background' )
&& ( get_background_color() !== get_theme_support( 'custom-background', 'default-color' ) || get_background_image() ) ) {
$classes[] = 'custom-background';
}
if ( has_custom_logo() ) {
$classes[] = 'wp-custom-logo';
}
if ( current_theme_supports( 'responsive-embeds' ) ) {
$classes[] = 'wp-embed-responsive';
}
$page = $wp_query->get( 'page' );
if ( ! $page || $page < 2 ) {
$page = $wp_query->get( 'paged' );
}
if ( $page && $page > 1 && ! is_404() ) {
$classes[] = 'paged-' . $page;
if ( is_single() ) {
$classes[] = 'single-paged-' . $page;
} elseif ( is_page() ) {
$classes[] = 'page-paged-' . $page;
} elseif ( is_category() ) {
$classes[] = 'category-paged-' . $page;
} elseif ( is_tag() ) {
$classes[] = 'tag-paged-' . $page;
} elseif ( is_date() ) {
$classes[] = 'date-paged-' . $page;
} elseif ( is_author() ) {
$classes[] = 'author-paged-' . $page;
} elseif ( is_search() ) {
$classes[] = 'search-paged-' . $page;
} elseif ( is_post_type_archive() ) {
$classes[] = 'post-type-paged-' . $page;
}
}
if ( ! empty( $css_class ) ) {
if ( ! is_array( $css_class ) ) {
$css_class = preg_split( '#\s+#', $css_class );
}
$classes = array_merge( $classes, $css_class );
} else {
// Ensure that we always coerce class to being an array.
$css_class = array();
}
$classes = array_map( 'esc_attr', $classes );
/**
* Filters the list of CSS body class names for the current post or page.
*
* @since 2.8.0
*
* @param string[] $classes An array of body class names.
* @param string[] $css_class An array of additional class names added to the body.
*/
$classes = apply_filters( 'body_class', $classes, $css_class );
return array_unique( $classes );
}
/**
* Determines whether the post requires password and whether a correct password has been provided.
*
* @since 2.7.0
*
* @param int|WP_Post|null $post An optional post. Global $post used if not provided.
* @return bool false if a password is not required or the correct password cookie is present, true otherwise.
*/
function post_password_required( $post = null ) {
$post = get_post( $post );
if ( empty( $post->post_password ) ) {
/** This filter is documented in wp-includes/post-template.php */
return apply_filters( 'post_password_required', false, $post );
}
if ( ! isset( $_COOKIE[ 'wp-postpass_' . COOKIEHASH ] ) ) {
/** This filter is documented in wp-includes/post-template.php */
return apply_filters( 'post_password_required', true, $post );
}
require_once ABSPATH . WPINC . '/class-phpass.php';
$hasher = new PasswordHash( 8, true );
$hash = wp_unslash( $_COOKIE[ 'wp-postpass_' . COOKIEHASH ] );
if ( ! str_starts_with( $hash, '$P$B' ) ) {
$required = true;
} else {
$required = ! $hasher->CheckPassword( $post->post_password, $hash );
}
/**
* Filters whether a post requires the user to supply a password.
*
* @since 4.7.0
*
* @param bool $required Whether the user needs to supply a password. True if password has not been
* provided or is incorrect, false if password has been supplied or is not required.
* @param WP_Post $post Post object.
*/
return apply_filters( 'post_password_required', $required, $post );
}
//
// Page Template Functions for usage in Themes.
//
/**
* The formatted output of a list of pages.
*
* Displays page links for paginated posts (i.e. including the ``
* Quicktag one or more times). This tag must be within The Loop.
*
* @since 1.2.0
* @since 5.1.0 Added the `aria_current` argument.
*
* @global int $page
* @global int $numpages
* @global int $multipage
* @global int $more
*
* @param string|array $args {
* Optional. Array or string of default arguments.
*
* @type string $before HTML or text to prepend to each link. Default is `
Pages:`.
* @type string $after HTML or text to append to each link. Default is `
';
// If the front page is a page, add it to the exclude list.
if ( 'page' === get_option( 'show_on_front' ) ) {
if ( ! empty( $list_args['exclude'] ) ) {
$list_args['exclude'] .= ',';
} else {
$list_args['exclude'] = '';
}
$list_args['exclude'] .= get_option( 'page_on_front' );
}
}
$list_args['echo'] = false;
$list_args['title_li'] = '';
$menu .= wp_list_pages( $list_args );
$container = sanitize_text_field( $args['container'] );
// Fallback in case `wp_nav_menu()` was called without a container.
if ( empty( $container ) ) {
$container = 'div';
}
if ( $menu ) {
// wp_nav_menu() doesn't set before and after.
if ( isset( $args['fallback_cb'] ) &&
'wp_page_menu' === $args['fallback_cb'] &&
'ul' !== $container ) {
$args['before'] = "
{$n}";
$args['after'] = '
';
}
$menu = $args['before'] . $menu . $args['after'];
}
$attrs = '';
if ( ! empty( $args['menu_id'] ) ) {
$attrs .= ' id="' . esc_attr( $args['menu_id'] ) . '"';
}
if ( ! empty( $args['menu_class'] ) ) {
$attrs .= ' class="' . esc_attr( $args['menu_class'] ) . '"';
}
$menu = "<{$container}{$attrs}>" . $menu . "{$container}>{$n}";
/**
* Filters the HTML output of a page-based menu.
*
* @since 2.7.0
*
* @see wp_page_menu()
*
* @param string $menu The HTML output.
* @param array $args An array of arguments. See wp_page_menu()
* for information on accepted arguments.
*/
$menu = apply_filters( 'wp_page_menu', $menu, $args );
if ( $args['echo'] ) {
echo $menu;
} else {
return $menu;
}
}
//
// Page helpers.
//
/**
* Retrieves HTML list content for page list.
*
* @uses Walker_Page to create HTML list content.
* @since 2.1.0
*
* @param array $pages
* @param int $depth
* @param int $current_page
* @param array $args
* @return string
*/
function walk_page_tree( $pages, $depth, $current_page, $args ) {
if ( empty( $args['walker'] ) ) {
$walker = new Walker_Page();
} else {
/**
* @var Walker $walker
*/
$walker = $args['walker'];
}
foreach ( (array) $pages as $page ) {
if ( $page->post_parent ) {
$args['pages_with_children'][ $page->post_parent ] = true;
}
}
return $walker->walk( $pages, $depth, $args, $current_page );
}
/**
* Retrieves HTML dropdown (select) content for page list.
*
* @since 2.1.0
* @since 5.3.0 Formalized the existing `...$args` parameter by adding it
* to the function signature.
*
* @uses Walker_PageDropdown to create HTML dropdown content.
* @see Walker_PageDropdown::walk() for parameters and return description.
*
* @param mixed ...$args Elements array, maximum hierarchical depth and optional additional arguments.
* @return string
*/
function walk_page_dropdown_tree( ...$args ) {
if ( empty( $args[2]['walker'] ) ) { // The user's options are the third parameter.
$walker = new Walker_PageDropdown();
} else {
/**
* @var Walker $walker
*/
$walker = $args[2]['walker'];
}
return $walker->walk( ...$args );
}
//
// Attachments.
//
/**
* Displays an attachment page link using an image or icon.
*
* @since 2.0.0
*
* @param int|WP_Post $post Optional. Post ID or post object.
* @param bool $fullsize Optional. Whether to use full size. Default false.
* @param bool $deprecated Deprecated. Not used.
* @param bool $permalink Optional. Whether to include permalink. Default false.
*/
function the_attachment_link( $post = 0, $fullsize = false, $deprecated = false, $permalink = false ) {
if ( ! empty( $deprecated ) ) {
_deprecated_argument( __FUNCTION__, '2.5.0' );
}
if ( $fullsize ) {
echo wp_get_attachment_link( $post, 'full', $permalink );
} else {
echo wp_get_attachment_link( $post, 'thumbnail', $permalink );
}
}
/**
* Retrieves an attachment page link using an image or icon, if possible.
*
* @since 2.5.0
* @since 4.4.0 The `$post` parameter can now accept either a post ID or `WP_Post` object.
*
* @param int|WP_Post $post Optional. Post ID or post object.
* @param string|int[] $size Optional. Image size. Accepts any registered image size name, or an array
* of width and height values in pixels (in that order). Default 'thumbnail'.
* @param bool $permalink Optional. Whether to add permalink to image. Default false.
* @param bool $icon Optional. Whether the attachment is an icon. Default false.
* @param string|false $text Optional. Link text to use. Activated by passing a string, false otherwise.
* Default false.
* @param array|string $attr Optional. Array or string of attributes. Default empty.
* @return string HTML content.
*/
function wp_get_attachment_link( $post = 0, $size = 'thumbnail', $permalink = false, $icon = false, $text = false, $attr = '' ) {
$_post = get_post( $post );
if ( empty( $_post ) || ( 'attachment' !== $_post->post_type ) || ! wp_get_attachment_url( $_post->ID ) ) {
return __( 'Missing Attachment' );
}
$url = wp_get_attachment_url( $_post->ID );
if ( $permalink ) {
$url = get_attachment_link( $_post->ID );
}
if ( $text ) {
$link_text = $text;
} elseif ( $size && 'none' !== $size ) {
$link_text = wp_get_attachment_image( $_post->ID, $size, $icon, $attr );
} else {
$link_text = '';
}
if ( '' === trim( $link_text ) ) {
$link_text = $_post->post_title;
}
if ( '' === trim( $link_text ) ) {
$link_text = esc_html( pathinfo( get_attached_file( $_post->ID ), PATHINFO_FILENAME ) );
}
/**
* Filters the list of attachment link attributes.
*
* @since 6.2.0
*
* @param array $attributes An array of attributes for the link markup,
* keyed on the attribute name.
* @param int $id Post ID.
*/
$attributes = apply_filters( 'wp_get_attachment_link_attributes', array( 'href' => $url ), $_post->ID );
$link_attributes = '';
foreach ( $attributes as $name => $value ) {
$value = 'href' === $name ? esc_url( $value ) : esc_attr( $value );
$link_attributes .= ' ' . esc_attr( $name ) . "='" . $value . "'";
}
$link_html = "$link_text";
/**
* Filters a retrieved attachment page link.
*
* @since 2.7.0
* @since 5.1.0 Added the `$attr` parameter.
*
* @param string $link_html The page link HTML output.
* @param int|WP_Post $post Post ID or object. Can be 0 for the current global post.
* @param string|int[] $size Requested image size. Can be any registered image size name, or
* an array of width and height values in pixels (in that order).
* @param bool $permalink Whether to add permalink to image. Default false.
* @param bool $icon Whether to include an icon.
* @param string|false $text If string, will be link text.
* @param array|string $attr Array or string of attributes.
*/
return apply_filters( 'wp_get_attachment_link', $link_html, $post, $size, $permalink, $icon, $text, $attr );
}
/**
* Wraps attachment in paragraph tag before content.
*
* @since 2.0.0
*
* @param string $content
* @return string
*/
function prepend_attachment( $content ) {
$post = get_post();
if ( empty( $post->post_type ) || 'attachment' !== $post->post_type ) {
return $content;
}
if ( wp_attachment_is( 'video', $post ) ) {
$meta = wp_get_attachment_metadata( get_the_ID() );
$atts = array( 'src' => wp_get_attachment_url() );
if ( ! empty( $meta['width'] ) && ! empty( $meta['height'] ) ) {
$atts['width'] = (int) $meta['width'];
$atts['height'] = (int) $meta['height'];
}
if ( has_post_thumbnail() ) {
$atts['poster'] = wp_get_attachment_url( get_post_thumbnail_id() );
}
$p = wp_video_shortcode( $atts );
} elseif ( wp_attachment_is( 'audio', $post ) ) {
$p = wp_audio_shortcode( array( 'src' => wp_get_attachment_url() ) );
} else {
$p = '
';
// Show the medium sized image representation of the attachment if available, and link to the raw file.
$p .= wp_get_attachment_link( 0, 'medium', false );
$p .= '
';
}
/**
* Filters the attachment markup to be prepended to the post content.
*
* @since 2.0.0
*
* @see prepend_attachment()
*
* @param string $p The attachment HTML output.
*/
$p = apply_filters( 'prepend_attachment', $p );
return "$p\n$content";
}
//
// Misc.
//
/**
* Retrieves protected post password form content.
*
* @since 1.0.0
*
* @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post.
* @return string HTML content for password form for password protected post.
*/
function get_the_password_form( $post = 0 ) {
$post = get_post( $post );
$label = 'pwbox-' . ( empty( $post->ID ) ? rand() : $post->ID );
$output = '
';
/**
* Filters the HTML output for the protected post password form.
*
* If modifying the password field, please note that the WordPress database schema
* limits the password field to 255 characters regardless of the value of the
* `minlength` or `maxlength` attributes or other validation that may be added to
* the input.
*
* @since 2.7.0
* @since 5.8.0 Added the `$post` parameter.
*
* @param string $output The password form HTML output.
* @param WP_Post $post Post object.
*/
return apply_filters( 'the_password_form', $output, $post );
}
/**
* Determines whether the current post uses a page template.
*
* This template tag allows you to determine if you are in a page template.
* You can optionally provide a template filename or array of template filenames
* and then the check will be specific to that template.
*
* For more information on this and similar theme functions, check out
* the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
* Conditional Tags} article in the Theme Developer Handbook.
*
* @since 2.5.0
* @since 4.2.0 The `$template` parameter was changed to also accept an array of page templates.
* @since 4.7.0 Now works with any post type, not just pages.
*
* @param string|string[] $template The specific template filename or array of templates to match.
* @return bool True on success, false on failure.
*/
function is_page_template( $template = '' ) {
if ( ! is_singular() ) {
return false;
}
$page_template = get_page_template_slug( get_queried_object_id() );
if ( empty( $template ) ) {
return (bool) $page_template;
}
if ( $template === $page_template ) {
return true;
}
if ( is_array( $template ) ) {
if ( ( in_array( 'default', $template, true ) && ! $page_template )
|| in_array( $page_template, $template, true )
) {
return true;
}
}
return ( 'default' === $template && ! $page_template );
}
/**
* Gets the specific template filename for a given post.
*
* @since 3.4.0
* @since 4.7.0 Now works with any post type, not just pages.
*
* @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post.
* @return string|false Page template filename. Returns an empty string when the default page template
* is in use. Returns false if the post does not exist.
*/
function get_page_template_slug( $post = null ) {
$post = get_post( $post );
if ( ! $post ) {
return false;
}
$template = get_post_meta( $post->ID, '_wp_page_template', true );
if ( ! $template || 'default' === $template ) {
return '';
}
return $template;
}
/**
* Retrieves formatted date timestamp of a revision (linked to that revisions's page).
*
* @since 2.6.0
*
* @param int|WP_Post $revision Revision ID or revision object.
* @param bool $link Optional. Whether to link to revision's page. Default true.
* @return string|false i18n formatted datetimestamp or localized 'Current Revision'.
*/
function wp_post_revision_title( $revision, $link = true ) {
$revision = get_post( $revision );
if ( ! $revision ) {
return $revision;
}
if ( ! in_array( $revision->post_type, array( 'post', 'page', 'revision' ), true ) ) {
return false;
}
/* translators: Revision date format, see https://www.php.net/manual/datetime.format.php */
$datef = _x( 'F j, Y @ H:i:s', 'revision date format' );
/* translators: %s: Revision date. */
$autosavef = __( '%s [Autosave]' );
/* translators: %s: Revision date. */
$currentf = __( '%s [Current Revision]' );
$date = date_i18n( $datef, strtotime( $revision->post_modified ) );
$edit_link = get_edit_post_link( $revision->ID );
if ( $link && current_user_can( 'edit_post', $revision->ID ) && $edit_link ) {
$date = "$date";
}
if ( ! wp_is_post_revision( $revision ) ) {
$date = sprintf( $currentf, $date );
} elseif ( wp_is_post_autosave( $revision ) ) {
$date = sprintf( $autosavef, $date );
}
return $date;
}
/**
* Retrieves formatted date timestamp of a revision (linked to that revisions's page).
*
* @since 3.6.0
*
* @param int|WP_Post $revision Revision ID or revision object.
* @param bool $link Optional. Whether to link to revision's page. Default true.
* @return string|false gravatar, user, i18n formatted datetimestamp or localized 'Current Revision'.
*/
function wp_post_revision_title_expanded( $revision, $link = true ) {
$revision = get_post( $revision );
if ( ! $revision ) {
return $revision;
}
if ( ! in_array( $revision->post_type, array( 'post', 'page', 'revision' ), true ) ) {
return false;
}
$author = get_the_author_meta( 'display_name', $revision->post_author );
/* translators: Revision date format, see https://www.php.net/manual/datetime.format.php */
$datef = _x( 'F j, Y @ H:i:s', 'revision date format' );
$gravatar = get_avatar( $revision->post_author, 24 );
$date = date_i18n( $datef, strtotime( $revision->post_modified ) );
$edit_link = get_edit_post_link( $revision->ID );
if ( $link && current_user_can( 'edit_post', $revision->ID ) && $edit_link ) {
$date = "$date";
}
$revision_date_author = sprintf(
/* translators: Post revision title. 1: Author avatar, 2: Author name, 3: Time ago, 4: Date. */
__( '%1$s %2$s, %3$s ago (%4$s)' ),
$gravatar,
$author,
human_time_diff( strtotime( $revision->post_modified_gmt ) ),
$date
);
/* translators: %s: Revision date with author avatar. */
$autosavef = __( '%s [Autosave]' );
/* translators: %s: Revision date with author avatar. */
$currentf = __( '%s [Current Revision]' );
if ( ! wp_is_post_revision( $revision ) ) {
$revision_date_author = sprintf( $currentf, $revision_date_author );
} elseif ( wp_is_post_autosave( $revision ) ) {
$revision_date_author = sprintf( $autosavef, $revision_date_author );
}
/**
* Filters the formatted author and date for a revision.
*
* @since 4.4.0
*
* @param string $revision_date_author The formatted string.
* @param WP_Post $revision The revision object.
* @param bool $link Whether to link to the revisions page, as passed into
* wp_post_revision_title_expanded().
*/
return apply_filters( 'wp_post_revision_title_expanded', $revision_date_author, $revision, $link );
}
/**
* Displays a list of a post's revisions.
*
* Can output either a UL with edit links or a TABLE with diff interface, and
* restore action links.
*
* @since 2.6.0
*
* @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post.
* @param string $type 'all' (default), 'revision' or 'autosave'
*/
function wp_list_post_revisions( $post = 0, $type = 'all' ) {
$post = get_post( $post );
if ( ! $post ) {
return;
}
// $args array with (parent, format, right, left, type) deprecated since 3.6.
if ( is_array( $type ) ) {
$type = ! empty( $type['type'] ) ? $type['type'] : $type;
_deprecated_argument( __FUNCTION__, '3.6.0' );
}
$revisions = wp_get_post_revisions( $post->ID );
if ( ! $revisions ) {
return;
}
$rows = '';
foreach ( $revisions as $revision ) {
if ( ! current_user_can( 'read_post', $revision->ID ) ) {
continue;
}
$is_autosave = wp_is_post_autosave( $revision );
if ( ( 'revision' === $type && $is_autosave ) || ( 'autosave' === $type && ! $is_autosave ) ) {
continue;
}
$rows .= "\t